using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;

namespace Comms.Drt {
    public class ServerGame {
        public class JoinRequest {
            public PeerData PeerData;

            public int ClientID;

            public string ClientName;

            public byte[] JoinRequestBytes;

            public double RequestTime;

            public bool Forwarded;

            public ServerClient AcceptedBy;

            public ServerClient RefusedBy;

            public int NextStateRequestIndex;

            public double NextStateRequestTime;

            public bool Processed;

            public JoinRequest(PeerData peerData, int clientID, string clientName, byte[] joinRequestBytes) {
                PeerData = peerData;
                ClientID = clientID;
                ClientName = clientName;
                JoinRequestBytes = joinRequestBytes;
                RequestTime = Comm.GetTime();
                NextStateRequestIndex = clientID;
            }
        }

        public List<ServerClient> ServerClients = new();

        public List<JoinRequest> JoinRequests = new();

        public List<int> Leaves = new();

        public List<ServerTickMessage> TickMessages = new();

        public DesyncDetector DesyncDetector;

        public int NextClientID;

        public int GameDescriptionStep;

        public byte[] GameDescriptionBytes;

        public double NextTickTime;

        public double NextDescriptionRequestTime;

        public int NextDescriptionRequestIndex;

        public DesyncDetectionMode DesyncDetectionMode { get; set; }

        public int DesyncDetectionPeriod { get; set; }

        public Server Server { get; }

        public int GameID { get; }

        public int Tick { get; set; }

        public IReadOnlyList<ServerClient> Clients => ServerClients;

        public ServerGame(Server server, PeerData creatorPeerData, int gameID, ClientCreateGameRequestMessage message) {
            Server = server;
            GameID = gameID;
            GameDescriptionBytes = message.GameDescriptionBytes;
            ServerClients.Add(new ServerClient(this, creatorPeerData, NextClientID++, message.ClientName));
            DesyncDetectionMode = server.Settings.DesyncDetectionMode;
            DesyncDetectionPeriod = (int)(server.Settings.DesyncDetectionPeriod / (Server.TickDuration / Server.StepsPerTick));
            DesyncDetector = new DesyncDetector(this);
        }

        public void Handle(ClientJoinGameRequestMessage message, PeerData peerData) {
            JoinRequest item = new(peerData, NextClientID++, message.ClientName, message.JoinRequestBytes);
            JoinRequests.Add(item);
        }

        public void Handle(ClientJoinGameAcceptedMessage message, ServerClient serverClient) {
            JoinRequest joinRequest = JoinRequests.FirstOrDefault(r => Equals(r.ClientID, message.ClientID));
            if (joinRequest != null) {
                if (joinRequest.RefusedBy != null) {
                    throw new ProtocolViolationException(
                        $"Join game accept from {serverClient.PeerData.Address} for client {message.ClientID} \"{joinRequest.ClientName}\" at {joinRequest.PeerData.Address}, which was already refused by {joinRequest.RefusedBy.PeerData.Address}."
                    );
                }
                if (joinRequest.AcceptedBy == null) {
                    joinRequest.AcceptedBy = serverClient;
                    Server.Peer.SendDataMessage(
                        serverClient.PeerData,
                        DeliveryMode.Reliable,
                        Server.MessageSerializer.Write(new ServerStateRequestMessage())
                    );
                    joinRequest.NextStateRequestTime = Comm.GetTime() + 1.0;
                }
                return;
            }
            throw new ProtocolViolationException(
                $"Join game accept from {serverClient.PeerData.Address} for non-existent join request with client ID {message.ClientID}."
            );
        }

        public void Handle(ClientJoinGameRefusedMessage message, ServerClient serverClient) {
            JoinRequest joinRequest = JoinRequests.FirstOrDefault(r => Equals(r.ClientID, message.ClientID));
            if (joinRequest != null) {
                if (joinRequest.AcceptedBy != null) {
                    throw new ProtocolViolationException(
                        $"Join game refuse from {serverClient.PeerData.Address} for client ID {message.ClientID}, which was already accepted by {joinRequest.AcceptedBy.PeerData.Address}."
                    );
                }
                if (joinRequest.RefusedBy == null) {
                    joinRequest.RefusedBy = serverClient;
                    Server.Peer.RefuseConnect(
                        joinRequest.PeerData,
                        Server.MessageSerializer.Write(new ServerConnectRefusedMessage { Reason = message.Reason })
                    );
                }
                return;
            }
            throw new ProtocolViolationException(
                $"Join game refuse from {serverClient.PeerData.Address} for non-existent client ID {message.ClientID}."
            );
        }

        public void Handle(ClientInputMessage message, ServerClient serverClient) {
            serverClient.InputsBytes.Add(message.InputBytes);
        }

        public void Handle(ClientStateMessage message, ServerClient serverClient) {
            foreach (JoinRequest joinRequest in JoinRequests) {
                if (!joinRequest.Processed
                    && joinRequest.AcceptedBy != null) {
                    int minimumTick = (message.Step + Server.StepsPerTick - 1) / Server.StepsPerTick;
                    ServerTickMessage[] array = TickMessages.Where(m => m.Tick >= minimumTick).ToArray();
                    if (array.Length != 0
                        && array[0].Tick != minimumTick) {
                        Server.InvokeWarning(
                            $"Not enough stored TickMessages for received state at step {message.Step}, earliest stored tick is {array[0].Tick}, tick required for this step is {minimumTick}."
                        );
                        continue;
                    }
                    ServerClient serverClient2 = new(this, joinRequest.PeerData, joinRequest.ClientID, joinRequest.ClientName);
                    ServerClients.Add(serverClient2);
                    Server.Peer.AcceptConnect(
                        serverClient2.PeerData,
                        Server.MessageSerializer.Write(
                            new ServerJoinGameAcceptedMessage {
                                GameID = GameID,
                                ClientID = serverClient2.ClientID,
                                TickDuration = Server.TickDuration,
                                StepsPerTick = Server.StepsPerTick,
                                DesyncDetectionMode = DesyncDetectionMode,
                                DesyncDetectionPeriod = DesyncDetectionPeriod,
                                Step = message.Step,
                                StateBytes = message.StateBytes,
                                TickMessages = array
                            }
                        )
                    );
                    Server.InvokeInformation(
                        $"Client \"{joinRequest.ClientName}\" at {joinRequest.PeerData.Address} joined game {GameID} at step {message.Step} (state size {message.StateBytes.Length} bytes)."
                    );
                    joinRequest.Processed = true;
                }
            }
        }

        public void Handle(ClientDesyncStateMessage message, ServerClient serverClient) {
            DesyncDetector.HandleDesyncState(message.Step, message.StateBytes, message.IsDeflated, serverClient);
        }

        public void Handle(ClientStateHashesMessage message, ServerClient serverClient) {
            DesyncDetector.HandleHashes(message.FirstHashStep, message.Hashes, serverClient);
        }

        public void Handle(ClientGameDescriptionMessage message, ServerClient serverClient) {
            if (message.Step > GameDescriptionStep) {
                GameDescriptionStep = message.Step;
                GameDescriptionBytes = message.GameDescriptionBytes;
            }
        }

        public void HandleDisconnect(ServerClient serverClient) {
            Server.InvokeInformation($"Client \"{serverClient.ClientName}\" at {serverClient.PeerData.Address} disconnected from game {GameID}.");
            ServerClients.Remove(serverClient);
            serverClient.PeerData.Tag = null;
            Leaves.Add(serverClient.ClientID);
        }

        public double Run(double time) {
            double val = double.MaxValue;
            if (NextTickTime == 0.0) {
                NextTickTime = CalculateNextTickTime(time);
            }
            if (time >= NextTickTime) {
                int num = 1 + (int)Math.Min(Math.Floor((time - NextTickTime) / Server.TickDuration), 10.0);
                for (int i = 0; i < num; i++) {
                    ServerTickMessage serverTickMessage = CreateTickMessage();
                    SendDataMessageToAllClients(serverTickMessage);
                    TickMessages.Add(serverTickMessage);
                    int tick = Tick + 1;
                    Tick = tick;
                }
                NextTickTime = CalculateNextTickTime(time);
            }
            val = Math.Min(val, NextTickTime);
            JoinRequests.RemoveAll(r => time - r.RequestTime >= Server.Settings.JoinRequestTimeout);
            if (ServerClients.Count > 0) {
                foreach (JoinRequest joinRequest in JoinRequests) {
                    if (!joinRequest.Processed
                        && joinRequest.AcceptedBy != null
                        && time >= joinRequest.NextStateRequestTime) {
                        ServerClient serverClient = ServerClients[joinRequest.NextStateRequestIndex % ServerClients.Count];
                        Server.Peer.SendDataMessage(
                            serverClient.PeerData,
                            DeliveryMode.Reliable,
                            Server.MessageSerializer.Write(new ServerStateRequestMessage())
                        );
                        joinRequest.NextStateRequestIndex++;
                        joinRequest.NextStateRequestTime = time + Server.Settings.StateRequestPeriod;
                        val = Math.Min(val, joinRequest.NextStateRequestTime);
                    }
                }
                if (NextDescriptionRequestTime == 0.0) {
                    NextDescriptionRequestTime = Server.Settings.GameDescriptionRequestPeriod;
                }
                if (time >= NextDescriptionRequestTime) {
                    ServerClient serverClient2 = ServerClients[NextDescriptionRequestIndex % ServerClients.Count];
                    Server.Peer.SendDataMessage(
                        serverClient2.PeerData,
                        DeliveryMode.Reliable,
                        Server.MessageSerializer.Write(new ServerGameDescriptionRequestMessage())
                    );
                    NextDescriptionRequestTime = time + Server.Settings.GameDescriptionRequestPeriod;
                    NextDescriptionRequestIndex++;
                }
                val = Math.Min(val, NextDescriptionRequestTime);
            }
            int num2 = (int)Math.Ceiling(Server.Settings.JoinRequestTimeout / Server.TickDuration) + 1;
            int earliestTickToKeep = Tick - num2;
            int num3 = TickMessages.FindIndex(m => m.Tick >= earliestTickToKeep);
            if (num3 > 0) {
                TickMessages.RemoveRange(0, num3);
            }
            DesyncDetector.Run();
            return val;
        }

        public GameDescription CreateGameDescription() => new() {
            GameID = GameID, Step = GameDescriptionStep, ClientsCount = ServerClients.Count, GameDescriptionBytes = GameDescriptionBytes
        };

        public ServerTickMessage CreateTickMessage() {
            ServerTickMessage serverTickMessage = new() {
                Tick = Tick, DesyncDetectedStep = DesyncDetector.DesyncDetectedStep, ClientsTickData = new List<ServerTickMessage.ClientTickData>()
            };
            foreach (ServerClient serverClient in ServerClients) {
                if (serverClient.InputsBytes.Count > 0) {
                    serverTickMessage.ClientsTickData.Add(
                        new ServerTickMessage.ClientTickData { ClientID = serverClient.ClientID, InputsBytes = serverClient.InputsBytes.ToList() }
                    );
                    serverClient.InputsBytes.Clear();
                }
            }
            foreach (JoinRequest joinRequest in JoinRequests) {
                if (!joinRequest.Forwarded) {
                    serverTickMessage.ClientsTickData.Add(
                        new ServerTickMessage.ClientTickData {
                            ClientID = joinRequest.ClientID, JoinAddress = joinRequest.PeerData.Address, JoinBytes = joinRequest.JoinRequestBytes
                        }
                    );
                    joinRequest.Forwarded = true;
                }
            }
            foreach (int leaf in Leaves) {
                serverTickMessage.ClientsTickData.Add(new ServerTickMessage.ClientTickData { ClientID = leaf, Leave = true });
            }
            Leaves.Clear();
            return serverTickMessage;
        }

        public void SendDataMessageToAllClients(Message message) {
            byte[] bytes = Server.MessageSerializer.Write(message);
            foreach (ServerClient serverClient in ServerClients) {
                Server.Peer.SendDataMessage(serverClient.PeerData, DeliveryMode.ReliableSequenced, bytes);
            }
        }

        public double CalculateNextTickTime(double time) => Math.Floor(time / Server.TickDuration + 1.0) * Server.TickDuration;
    }
}